
interface AssignOptions {
    onlyUseToField?: boolean,
    hasBuildNewObject?: boolean,
    hasArrayPush?: boolean,
    hasArrayDistinct?: boolean
}
const defaultOptions: AssignOptions = {
    onlyUseToField: false,
    hasBuildNewObject: true,
    hasArrayPush: false,
    hasArrayDistinct: false
}
interface DiffOptions {
    ignore: string[]
}
class ObjectAssign {
    assign<T>(options: AssignOptions, ...forms: any[]): T;
    assign() {

        var options = this.merge(defaultOptions, arguments[0], { onlyUseToField: true, hasBuildNewObject: true })

        var to = arguments[0];
        var beginLen = 1;

        if ((to.hasOwnProperty('onlyUseToField') && typeof to['onlyUseToField'] === 'boolean')
            || (to.hasOwnProperty('hasBuildNewObject') && typeof to['hasBuildNewObject'] === 'boolean')
            || (to.hasOwnProperty('hasArrayPush') && typeof to['hasArrayPush'] === 'boolean')) {
            to = arguments[1]
            beginLen = 2
        }
        // console.log('\r\n\r\noptions             :' + JSON.stringify(options))

        // to = this.merge(to, options, { onlyUseToField: true })
        for (var index = beginLen; index < arguments.length; index++) {
            var element = arguments[index];
            var _oldTo = JSON.stringify(to);
            to = this.merge(to, element, options)

            // console.log('\r\nassign finish', 'old to:' + _oldTo, '\r\nmerge element       :' + JSON.stringify(element), '\r\nfinish              :' + JSON.stringify(to))
        }
        return to
    }
    merge<T>(to: T, form: any, options: AssignOptions): T {
        if (to === form)
            return to;
        if (to === null || to === undefined || form === null || form === undefined)
            return to;
        if (!options)
            options = defaultOptions
        var _to: any = to;
        if (options.hasBuildNewObject)
            _to = this.merge({}, to, { hasBuildNewObject: false, onlyUseToField: false })
        for (var key in form) {
            if (!form.hasOwnProperty(key))
                continue;

            var element = form[key];
            if (element.constructor == Object) {
                if (!_to.hasOwnProperty(key) && options.onlyUseToField)
                    continue
                _to[key] = this.merge(_to[key] || {}, element, options)

            } else if (element instanceof Array) {
                if (!_to.hasOwnProperty(key) && options.onlyUseToField)
                    continue
                if (!_to[key])
                    _to[key] = []
                if (options.hasArrayPush) {

                    if (options.hasArrayDistinct) {
                        for (var elIndex = 0; elIndex < element.length; elIndex++) {
                            var arrEl = element[elIndex];
                            if (_to[key].indexOf(arrEl) < 0)
                                _to[key].push(arrEl)
                        }
                    } else
                        _to[key].push.apply(_to[key], element)
                }
                else
                    _to[key] = element

            } else {
                if (!_to.hasOwnProperty(key) && options.onlyUseToField)
                    continue
                _to[key] = element
            }

        }
        return _to;
    }
    sec_obj(obj: any) {
        if (typeof obj === 'undefined') return obj;
        return JSON.parse(JSON.stringify(obj))
    }
    diff(left: any, right: any, options: DiffOptions) {
        if (!options) options = { ignore: [] }

        if (left === right) return undefined;
        if (typeof left !== typeof right)
            return { left: this.sec_obj(left), right: this.sec_obj(right) };

        if (Array.isArray(left)) {
            let _diff = [];
            for (let index = 0; index < Math.max(left.length, right.length); index++) {
                const element = left[index];
                if (element !== right[index])
                    _diff[index] = { left: this.sec_obj(left[index]), right: this.sec_obj(right[index]) }
            }
            return _diff.length ? _diff : undefined
        }

        else if (typeof left === 'object') {
            let _diff = new Map<string, any>();
            for (const key in left) {
                if (left.hasOwnProperty(key)) {
                    if (options.ignore.indexOf(key) == -1 && left[key] !== right[key]) {
                        if (typeof left[key] === 'object' || Array.isArray(left[key]) || typeof right[key] === 'object' || Array.isArray(right[key])) {
                            var cDiff = this.diff(left[key], right[key], options);
                            cDiff && _diff.set(key, cDiff);
                        } else
                            _diff.set(key, { left: this.sec_obj(left[key]), right: this.sec_obj(right[key]) });
                    }
                }
            }

            for (const key in right) {
                if (right.hasOwnProperty(key)) {
                    if (options.ignore.indexOf(key) == -1 && left[key] !== right[key]) {
                        if (typeof left[key] === 'object' || Array.isArray(left[key]) || typeof right[key] === 'object' || Array.isArray(right[key])) {
                            var cDiff = this.diff(left[key], right[key], options);
                            cDiff && _diff.set(key, cDiff);
                        } else
                            _diff.set(key, { left: this.sec_obj(left[key]), right: this.sec_obj(right[key]) });
                    }
                }
            }

            return _diff.size ? _diff : undefined;

        } else
            return { left: this.sec_obj(left), right: this.sec_obj(right) };
    }
}

const instance = new ObjectAssign();

export = instance